package org.springmodules.jcr.jackrabbit;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.transaction.xa.XAResource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.InvalidIsolationLevelException;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.SmartTransactionObject;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springmodules.jcr.SessionFactory;
import org.springmodules.jcr.SessionFactoryUtils;
import org.springmodules.jcr.SessionHolder;
import org.springmodules.jcr.jackrabbit.support.UserTxSessionHolder;
/**
* PlatformTransactionManager implementation for a single JCR SessionFactory.
*
* Binds a Jcr session from the specified SessionFactory to the thread,
* potentially allowing for one thread session per session factory.
*
* <p>
* This local strategy is an alternative to executing JCR operations within JTA
* transactions. Its advantage is that it is able to work in any environment,
* for example a standalone application or a test suite. It is <i>not</i> able
* to provide XA transactions, for example to share transactions with data
* access.
*
* <p>
* JcrTemplate will auto-detect such thread-bound connection/session pairs and
* automatically participate in them. There is currently no support for letting
* plain JCR code participate in such transactions.
*
* <p>
* This transaction strategy will typically be used in combination with a single
* JCR Repository for all JCR access to save resources, typically in a
* standalone application.
*
* @see javax.jcr.RepositoryException
* @see org.springframework.transaction.support.AbstractPlatformTransactionManager
*
* @author Costin Leau
* @author Guillaume Bort <guillaume.bort@zenexity.fr>
*
*/
public class LocalTransactionManager extends AbstractPlatformTransactionManager implements InitializingBean {
private SessionFactory sessionFactory;
/**
* @return Returns the sessionFactory.
*/
public SessionFactory getSessionFactory() {
return sessionFactory;
}
/**
* Create a new JcrTransactionManager instance.
*
*/
public LocalTransactionManager() {
}
/**
* Create a new JcrTransactionManager instance.
*
* @param sessionFactory
* Repository to manage transactions for
*/
public LocalTransactionManager(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public void afterPropertiesSet() throws Exception {
if (getSessionFactory() == null)
throw new IllegalArgumentException("repository is required");
}
protected Object doGetTransaction() throws TransactionException {
JcrTransactionObject txObject = new JcrTransactionObject();
if (TransactionSynchronizationManager.hasResource(getSessionFactory())) {
UserTxSessionHolder sessionHolder = (UserTxSessionHolder) TransactionSynchronizationManager.getResource(getSessionFactory());
if (logger.isDebugEnabled()) {
logger.debug("Found thread-bound session [" + sessionHolder.getSession() + "] for JCR transaction");
}
txObject.setSessionHolder(sessionHolder, false);
}
return txObject;
}
protected boolean isExistingTransaction(Object transaction) throws TransactionException {
return ((JcrTransactionObject) transaction).hasTransaction();
}
protected void doBegin(Object transaction, TransactionDefinition transactionDefinition) throws TransactionException {
if (transactionDefinition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
throw new InvalidIsolationLevelException("JCR does not support an isolation level concept");
}
Session session = null;
try {
JcrTransactionObject txObject = (JcrTransactionObject) transaction;
if (txObject.getSessionHolder() == null) {
// get the new session
Session newSession = sessionFactory.getSession();
// make sure we have an XAResource
if (!(newSession instanceof XAResource))
throw new IllegalArgumentException("transactions are not supported by your Jcr Repository");
if (logger.isDebugEnabled()) {
logger.debug("Opened new session [" + newSession + "] for JCR transaction");
}
txObject.setSessionHolder(new UserTxSessionHolder(newSession), true);
}
UserTxSessionHolder sessionHolder = txObject.getSessionHolder();
sessionHolder.setSynchronizedWithTransaction(true);
session = sessionHolder.getSession();
/*
* We have no notion of flushing inside a JCR session
*
if (transactionDefinition.isReadOnly() && txObject.isNewSessionHolder()) {
sessionHolder.setReadOnly(true);
}
if (!transactionDefinition.isReadOnly() && !txObject.isNewSessionHolder()) {
if (sessionHolder.isReadOnly()) {
sessionHolder.setReadOnly(false);
}
}
*/
// start the transaction
sessionHolder.getTransaction().begin();
// Register transaction timeout.
if (transactionDefinition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getSessionHolder().setTimeoutInSeconds(transactionDefinition.getTimeout());
}
// Bind the session holder to the thread.
if (txObject.isNewSessionHolder()) {
TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder);
}
}
catch (Exception ex) {
SessionFactoryUtils.releaseSession(session, getSessionFactory());
throw new CannotCreateTransactionException("Could not open JCR session for transaction", ex);
}
}
protected Object doSuspend(Object transaction) {
JcrTransactionObject txObject = (JcrTransactionObject) transaction;
txObject.setSessionHolder(null, false);
SessionHolder sessionHolder = (UserTxSessionHolder) TransactionSynchronizationManager.unbindResource(getSessionFactory());
return new SuspendedResourcesHolder(sessionHolder);
}
protected void doResume(Object transaction, Object suspendedResources) {
SuspendedResourcesHolder resourcesHolder = (SuspendedResourcesHolder) suspendedResources;
if (TransactionSynchronizationManager.hasResource(getSessionFactory())) {
// From non-transactional code running in active transaction
// synchronization
// -> can be safely removed, will be closed on transaction
// completion.
TransactionSynchronizationManager.unbindResource(getSessionFactory());
}
TransactionSynchronizationManager.bindResource(getSessionFactory(), resourcesHolder.getSessionHolder());
}
protected void doCommit(DefaultTransactionStatus status) {
JcrTransactionObject txObject = (JcrTransactionObject) status.getTransaction();
if (status.isDebug()) {
logger.debug("Committing JCR transaction on session [" + txObject.getSessionHolder().getSession() + "]");
}
try {
txObject.getSessionHolder().getTransaction().commit();
} catch (Exception ex) {
// assumably from commit call to the underlying JCR repository
throw new TransactionSystemException("Could not commit JCR transaction", ex);
}
}
protected void doRollback(DefaultTransactionStatus status) {
JcrTransactionObject txObject = (JcrTransactionObject) status.getTransaction();
if (status.isDebug()) {
logger.debug("Rolling back JCR transaction on session [" + txObject.getSessionHolder().getSession() + "]");
}
try {
txObject.getSessionHolder().getTransaction().rollback();
} catch (Exception ex) {
throw new TransactionSystemException("Could not roll back JCR transaction", ex);
} finally {
if (!txObject.isNewSessionHolder()) {
// Clear all pending inserts/updates/deletes in the Session.
// Necessary for pre-bound Sessions, to avoid inconsistent
// state.
try {
txObject.getSessionHolder().getSession().refresh(false);
} catch (RepositoryException e) {
// we already throw an exception (hold back this one).
}
}
}
}
protected void doSetRollbackOnly(DefaultTransactionStatus status) {
JcrTransactionObject txObject = (JcrTransactionObject) status.getTransaction();
if (status.isDebug()) {
logger.debug("Setting JCR transaction on session [" + txObject.getSessionHolder().getSession() + "] rollback-only");
}
txObject.setRollbackOnly();
}
protected void doCleanupAfterCompletion(Object transaction) {
JcrTransactionObject txObject = (JcrTransactionObject) transaction;
// Remove the session holder from the thread.
if (txObject.isNewSessionHolder()) {
TransactionSynchronizationManager.unbindResource(getSessionFactory());
}
Session session = txObject.getSessionHolder().getSession();
if (txObject.isNewSessionHolder()) {
if (logger.isDebugEnabled()) {
logger.debug("Closing JCR session [" + session + "] after transaction");
}
SessionFactoryUtils.releaseSession(session, sessionFactory);
} else {
if (logger.isDebugEnabled()) {
logger.debug("Not closing pre-bound JCR session [" + session + "] after transaction");
}
}
txObject.getSessionHolder().clear();
}
/**
* Internal transaction object.
*
* @see org.springframework.transaction.support.SmartTransactionObject
*
*/
private static class JcrTransactionObject implements SmartTransactionObject {
private UserTxSessionHolder sessionHolder;
private boolean newSessionHolder;
public void setSessionHolder(UserTxSessionHolder sessionHolder, boolean newSessionHolder) {
this.sessionHolder = sessionHolder;
this.newSessionHolder = newSessionHolder;
}
public UserTxSessionHolder getSessionHolder() {
return sessionHolder;
}
public boolean isNewSessionHolder() {
return newSessionHolder;
}
public boolean hasTransaction() {
return (this.sessionHolder != null && this.sessionHolder.getTransaction() != null);
}
public void setRollbackOnly() {
getSessionHolder().setRollbackOnly();
}
public boolean isRollbackOnly() {
return getSessionHolder().isRollbackOnly();
}
public void flush() {
}
}
/**
* Holder for suspended resources. Used internally by doSuspend and
* doResume.
*/
private static class SuspendedResourcesHolder {
private final SessionHolder sessionHolder;
private SuspendedResourcesHolder(SessionHolder sessionHolder) {
this.sessionHolder = sessionHolder;
}
private SessionHolder getSessionHolder() {
return sessionHolder;
}
}
/**
* @param sessionFactory
* The sessionFactory to set.
*/
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
}